﻿// --------------------------------------------------------------------------------------------------------------------
// <copyright file="SPQueryBuilder.cs" company="Collaboris Ltd.">
//   Copyright (c) Collaboris Ltd. All rights Reserved.
// </copyright>
// <summary>
//   Simplifies the creation of CAML queries by creating a CAML query expression from a  a <see cref="QueryExpression" />
// </summary>
// --------------------------------------------------------------------------------------------------------------------

namespace Collaboris.DataAccess.SharePoint
{
    #region Imports

    using System;
    using System.IO;
    using System.Text;
    using System.Xml;

    using Microsoft.SharePoint;

    #endregion

    /// <summary>
    /// Simplifies the creation of CAML queries by creating a CAML query expression from a  a <see cref="QueryExpression"/>
    /// </summary>
    public class SPQueryBuilder
    {
        #region Constants

        /// <summary>
        /// The publishing end date field.
        /// </summary>
        private const string PublishingEndDateField = "PublishingExpirationDate";

        /// <summary>
        /// The publishing start date field.
        /// </summary>
        private const string PublishingStartDateField = "PublishingStartDate";

        #endregion

        #region Fields

        /// <summary>
        /// The columns.
        /// </summary>
        private QueryColumnCollection columns = new QueryColumnCollection();

        /// <summary>
        /// The max rows.
        /// </summary>
        private uint maxRows;

        /// <summary>
        /// The publishing enabled.
        /// </summary>
        private bool publishingEnabled;

        /// <summary>
        /// The sort param collection.
        /// </summary>
        private SortParamCollection sortParamCollection = new SortParamCollection();

        #endregion

        #region Properties

        /// <summary>
        /// Gets or sets SortParamCollection.
        /// </summary>
        public SortParamCollection SortParamCollection
        {
            get
            {
                return this.sortParamCollection;
            }

            set
            {
                this.sortParamCollection = value;
            }
        }

        /// <summary>
        /// Gets or sets MaxRows.
        /// </summary>
        public uint MaxRows
        {
            get
            {
                return this.maxRows;
            }

            set
            {
                this.maxRows = value;
            }
        }

        /// <summary>
        /// Gets or sets a value indicating whether PublishingEnabled.
        /// </summary>
        public bool PublishingEnabled
        {
            get
            {
                return this.publishingEnabled;
            }

            set
            {
                this.publishingEnabled = value;
            }
        }

        /// <summary>
        /// Gets or sets Columns.
        /// </summary>
        public QueryColumnCollection Columns
        {
            get
            {
                return this.columns;
            }

            set
            {
                this.columns = value;
            }
        }

        #endregion

        #region Helper methods

        /// <summary>
        /// Translates the <see cref="ComparisonOperator"/> from a <see cref="QueryExpression"/> to 
        /// a CAML operator
        /// </summary>
        /// <param name="compOperator">
        /// The comp Operator.
        /// </param>
        /// <returns>
        /// String with the correspondent CAML operator
        /// </returns>
        protected static string TranslateComparisonOperator(ComparisonOperator compOperator)
        {
            switch (compOperator)
            {
                case ComparisonOperator.Eq:
                    return "Eq";

                case ComparisonOperator.Begins:
                    return "BeginsWith";

                case ComparisonOperator.Contains:
                    return "Contains";

                case ComparisonOperator.DateRangesOverlap:
                    return "DateRangesOverlap";

                case ComparisonOperator.Greater:
                    return "Gt";

                case ComparisonOperator.GreaterOrEqual:
                    return "Geq";

                case ComparisonOperator.IsNotNull:
                    return "IsNotNull";

                case ComparisonOperator.IsNull:
                    return "IsNull";

                case ComparisonOperator.Less:
                    return "Lt";

                case ComparisonOperator.LessOrEqual:
                    return "Leq";

                case ComparisonOperator.NotEqual:
                    return "Neq";
            }

            return null;
        }

        /// <summary>
        /// The translate logical operator.
        /// </summary>
        /// <param name="logicalOperator">
        /// The logical operator.
        /// </param>
        /// <returns>
        /// The translate logical operator.
        /// </returns>
        protected static string TranslateLogicalOperator(LogicalOperator logicalOperator)
        {
            switch (logicalOperator)
            {
                case LogicalOperator.Or:
                    return "Or";

                case LogicalOperator.And:
                    return "And";
            }

            return null;
        }

        /// <summary>
        /// The translate sort operator.
        /// </summary>
        /// <param name="sortOperator">
        /// The sort operator.
        /// </param>
        /// <returns>
        /// The translate sort operator.
        /// </returns>
        protected static string TranslateSortOperator(SortOperator sortOperator)
        {
            switch (sortOperator)
            {
                case SortOperator.Asc:
                    return "True";

                case SortOperator.Desc:
                    return "False";
            }

            return null;
        }

        /// <summary>
        /// The build view.
        /// </summary>
        /// <param name="columns">
        /// </param>
        /// <returns>
        /// The build view.
        /// </returns>
        protected static string BuildView(QueryColumnCollection columns)
        {
            var viewColumns = new StringBuilder();

            foreach (string columnName in columns)
            {
                viewColumns.AppendFormat(@"<FieldRef Name=""{0}""/>", columnName);
            }

            return viewColumns.ToString();
        }

        /// <summary>
        /// Builds a query so that only the rows that are between publishing start date and publishing end date are returned.
        /// </summary>
        /// <returns>
        /// </returns>
        protected static QueryExpression BuildPublishingQuery()
        {
            var startDateQuery = new QueryExpression(new QueryParam(PublishingStartDateField, "<Today/>", "DateTime", ComparisonOperator.LessOrEqual), 
                    LogicalOperator.Or, 
                                                     new QueryParam(PublishingStartDateField, string.Empty, "DateTime", ComparisonOperator.IsNull));

            var endtDateQuery = new QueryExpression(new QueryParam(PublishingEndDateField, "<Today/>", "DateTime", ComparisonOperator.Greater), 
                                                    LogicalOperator.Or, 
                                                    new QueryParam(PublishingEndDateField, string.Empty, "DateTime", ComparisonOperator.IsNull));

            var publishingQuery = new QueryExpression(startDateQuery, LogicalOperator.And, endtDateQuery);

            return publishingQuery;
        }

        /// <summary>
        /// The write query.
        /// </summary>
        /// <param name="query">
        /// The query.
        /// </param>
        /// <param name="queryWriter">
        /// The query writer.
        /// </param>
        private static void WriteQuery(QueryExpression query, XmlTextWriter queryWriter)
        {
            // if it is a simple expression it will only contain a left operand
            // write the left operand and return
            if (query.QueryType == ExpressionType.Simple)
            {
                WriteQueryElement(queryWriter, query.QueryValue);
                return;
            }

            // write the operator start tag
            queryWriter.WriteStartElement(TranslateLogicalOperator(query.Operator));

            WriteQuery(query.LeftOperand, queryWriter);
            WriteQuery(query.RightOperand, queryWriter);

            // write the operator end tag
            queryWriter.WriteFullEndElement();
        }

        /// <summary>
        /// The write query element.
        /// </summary>
        /// <param name="queryWriter">
        /// The query writer.
        /// </param>
        /// <param name="param">
        /// The param.
        /// </param>
        /// <exception cref="ArgumentNullException">
        /// </exception>
        private static void WriteQueryElement(XmlWriter queryWriter, QueryParam param)
        {
            if (queryWriter == null)
            {
                throw new ArgumentNullException("queryWriter");
            }

            // Comparison Operator (i.e. <Eq>)  
            queryWriter.WriteStartElement(TranslateComparisonOperator(param.ComparisonOp));

            // <FieldRef Name="....."/>
            queryWriter.WriteStartElement("FieldRef");
            queryWriter.WriteAttributeString("Name", param.ParamName);
            queryWriter.WriteEndElement();

            if (param.ComparisonOp != ComparisonOperator.IsNull && param.ComparisonOp != ComparisonOperator.IsNotNull)
            {
                // <Value Type="......">.....</Value>
                queryWriter.WriteStartElement("Value");
                queryWriter.WriteAttributeString("Type", param.ParamType);
                queryWriter.WriteValue(param.ParamValue);
                queryWriter.WriteFullEndElement();
            }

            // close comparison operator
            queryWriter.WriteFullEndElement();
        }

        /// <summary>
        /// Generates the sort element and writes it to the queryWriter
        /// </summary>
        /// <param name="queryWriter">
        /// </param>
        protected virtual void BuildSortQuery(XmlTextWriter queryWriter)
        {
            if (queryWriter == null)
            {
                throw new ArgumentNullException("queryWriter");
            }

            queryWriter.WriteStartElement("OrderBy");
            foreach (SortParam param in this.sortParamCollection)
            {
                queryWriter.WriteStartElement("FieldRef");
                queryWriter.WriteAttributeString("Name", param.OrderByField);
                queryWriter.WriteAttributeString("Ascending", TranslateSortOperator(param.Direction));
                queryWriter.WriteEndElement();
            }

            queryWriter.WriteFullEndElement();
        }

        #endregion

        #region Public methods

        /// <summary>
        /// Builds a CAML query from a query expression
        /// </summary>
        /// <param name="queryExpression">
        /// </param>
        /// <returns>
        /// The build complex query.
        /// </returns>
        public virtual string BuildCamlQuery(QueryExpression queryExpression)
        {
            bool hasQueryParams = queryExpression != null;

            var strWriter = new StringWriter();
            var queryWriter = new XmlTextWriter(strWriter);

            QueryExpression query = queryExpression;

            // If publishing is enabled then we need to 'and' the search query with publishing query
            if (this.publishingEnabled)
            {
                QueryExpression publishingQuery = BuildPublishingQuery();

                if (queryExpression == null)
                {
                    query = publishingQuery;
                }
                else
                {
                    query = new QueryExpression(publishingQuery, LogicalOperator.And, queryExpression);
                }
            }

            // If there are no query params then no <where> clause is required.
            if (hasQueryParams || this.publishingEnabled)
            {
                queryWriter.WriteStartElement("Where");

                WriteQuery(query, queryWriter);

                queryWriter.WriteFullEndElement();
            }

            // Build the SortBy element
            if (this.sortParamCollection.Count > 0)
            {
                this.BuildSortQuery(queryWriter);
            }

            queryWriter.Close();

            return strWriter.ToString();
        }

        /// <summary>
        /// The build query.
        /// </summary>
        /// <param name="queryParam">
        /// The query param.
        /// </param>
        /// <returns>
        /// </returns>
        public SPQuery BuildQuery(QueryParam queryParam)
        {
            return this.BuildQuery(new QueryExpression(queryParam));
        }

        /// <summary>
        /// The build query.
        /// </summary>
        /// <param name="queryExpression">
        /// The query expression.
        /// </param>
        /// <returns>
        /// </returns>
        public SPQuery BuildQuery(QueryExpression queryExpression)
        {
            /*
            if (queryExpression == null)
                throw new ArgumentNullException("queryExpression", "parameter cannot be null");
             */
            var spQuery = new SPQuery();

            string queryColumns = BuildView(this.columns);
            string queryText = this.BuildCamlQuery(queryExpression);

            spQuery.ViewFields = queryColumns;
            spQuery.Query = queryText;

            if (this.maxRows != 0)
            {
                spQuery.RowLimit = this.maxRows;
            }

            return spQuery;
        }

        #endregion
    }
}